/*
 * Copyright (c) 2011 Wang Wenlin
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/keyvalq_struct.h>

#ifdef _EVENT_HAVE_NETINET_IN_H
#include <netinet/in.h>
# ifdef _XOPEN_SOURCE_EXTENDED
#  include <arpa/inet.h>
# endif
#endif

#define HTTP_FOBIDDEN		403
#define HTTP_DEFAULT_PORT	80

static const struct table_entry {
	const char *extension;
	const char *content_type;
} content_type_table[] = {
	{ "txt", "text/plain" },
	{ "c", "text/plain" },
	{ "h", "text/plain" },
	{ "html", "text/html" },
	{ "htm", "text/htm" },
	{ "css", "text/css" },
	{ "js", "text/javascript" },
	{ "gif", "image/gif" },
	{ "jpg", "image/jpeg" },
	{ "jpeg", "image/jpeg" },
	{ "png", "image/png" },
	{ "pdf", "application/pdf" },
	{ "ps", "application/postsript" },
	{ NULL, NULL },
};

static const char empty_gif[] = {
    'G', 'I', 'F', '8', '9', 'a',  /* header                                 */

                                   /* logical screen descriptor              */
    0x01, 0x00,                    /* logical screen width                   */
    0x01, 0x00,                    /* logical screen height                  */
    0x80,                          /* global 1-bit color table               */
    0x01,                          /* background color #1                    */
    0x00,                          /* no aspect ratio                        */

                                   /* global color table                     */
    0x00, 0x00, 0x00,              /* #0: black                              */
    0xff, 0xff, 0xff,              /* #1: white                              */

                                   /* graphic control extension              */
    0x21,                          /* extension introducer                   */
    0xf9,                          /* graphic control label                  */
    0x04,                          /* block size                             */
    0x01,                          /* transparent color is given,            */
                                   /*     no disposal specified,             */
                                   /*     user input is not expected         */
    0x00, 0x00,                    /* delay time                             */
    0x01,                          /* transparent color #1                   */
    0x00,                          /* block terminator                       */

                                   /* image descriptor                       */
    0x2c,                          /* image separator                        */
    0x00, 0x00,                    /* image left position                    */
    0x00, 0x00,                    /* image top position                     */
    0x01, 0x00,                    /* image width                            */
    0x01, 0x00,                    /* image height                           */
    0x00,                          /* no local color table, no interlaced    */

                                   /* table based image data                 */
    0x02,                          /* LZW minimum code size,                 */
                                   /*     must be at least 2-bit             */
    0x02,                          /* block size                             */
    0x4c, 0x01,                    /* compressed bytes 01_001_100, 0000000_1 */
                                   /* 100: clear code                        */
                                   /* 001: 1                                 */
                                   /* 101: end of information code           */
    0x00,                          /* block terminator                       */

    0x3B                           /* trailer                                */
};

/* Try to guess a good content-type for 'path' */
static const char *
guess_content_type(const char *path)
{
	const char *last_period, *extension;
	const struct table_entry *ent;

	last_period = strrchr(path, '.');
	if (!last_period || strchr(last_period, '/'))
		goto not_found; /* no exension */

	extension = last_period + 1;
	for (ent = &content_type_table[0]; ent->extension; ++ent) {
		if (!evutil_ascii_strcasecmp(ent->extension, extension))
			return ent->content_type;
	}

not_found:
	return (NULL);
}

/*
[request_count] src_ip: /empty_gif?1223245,12313141245,12313141232,123
[request_count] src_ip: /empty_gif?1223245,12313141267,12313141245,134
[request_count] src_ip: /10k.gif?1223245,12313141245,12313141232,123
[request_count] src_ip: /10k.gif?1223245,12313141267,12313141245,134
*/

/* Access log fd */
static int log_fd = -1;
static long long request_count = 0;
static pthread_t log_thr;

struct logbuf_t {
	volatile int spinlock;
	volatile int count;
	struct evbuffer *evb;
};

static struct logbuf_t logbuf0, logbuf1;
static struct logbuf_t *volatile logbuf = &logbuf0;

#define LOG_BUF_MAX_SIZE  (256 * 1024 * 1024)

#define FAA(var, v) \
	__sync_fetch_and_add((var), (v))

#define FAS(var, v) \
	__sync_fetch_and_sub((var), (v))

#define CAS(var, ov, nv) \
	__sync_bool_compare_and_swap ((var), (ov), (nv))

static void
write_log_thread(void *arg)
{
	struct logbuf_t *buf;

	for (; buf = logbuf; usleep(50 * 1000)) {
		if (buf->count > 0) {
			if (FAA(&buf->spinlock, 1) != 0)
				while (buf->spinlock != 1)
					usleep(10 * 1000); // 10ms

			do {
				evbuffer_write(buf->evb, log_fd);
			} while (evbuffer_get_length(buf->evb) > 0);

			buf->count = 0;			
			FAS(&buf->spinlock, 1);
		}
	}
}

static void
access_log(struct evhttp_request *req)
{
	struct logbuf_t *buf = logbuf;
	
	if (!CAS(&buf->spinlock, 0, 1)) {
		buf = (buf != &logbuf0)
				? &logbuf0 : &logbuf1;
		FAA(&buf->spinlock, 1);
		logbuf = buf;
	}

	if (evbuffer_get_length(buf->evb) >= LOG_BUF_MAX_SIZE) goto done;

	const char *remoteaddr;
	ev_uint16_t remoteport;
	struct evhttp_connection *conn = evhttp_request_get_connection(req);
	evhttp_connection_get_peer(conn, (char **)&remoteaddr, &remoteport);

	const char *uri = evhttp_request_get_uri(req);
	evbuffer_add_printf(buf->evb, "[%012lld] %s: %s\n", request_count, remoteaddr, uri);

done:
	buf->count++;
	FAS(&buf->spinlock, 1);
}

/* Callback used for the /log URI, log request and gives
 * back a trivial 200 ok */
static void
log_request_cb(struct evhttp_request *req, void *arg)
{
	++request_count;
	if (evhttp_request_get_command(req) != EVHTTP_REQ_GET)
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
	else {
		access_log(req);
		evhttp_send_reply(req, HTTP_OK, "OK", NULL);
	}
}

/* Callback used for the /ping URI, only gives back
 * a trivial 200 ok */
static void
ping_request_cb(struct evhttp_request *req, void *arg)
{
	++request_count;
	if (evhttp_request_get_command(req) != EVHTTP_REQ_GET)
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
	else
		evhttp_send_reply(req, HTTP_OK, "OK", NULL);
}

/* Callback used for the /empty_gif URI. */
static void
emptygif_request_cb(struct evhttp_request *req, void *arg)
{
	++request_count;
	if (evhttp_request_get_command(req) != EVHTTP_REQ_GET)
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
	else {
		struct evbuffer *evb = evbuffer_new();
		if (!evb)
			evhttp_send_error(req, HTTP_INTERNAL, 0);
		else {
			evhttp_add_header(evhttp_request_get_output_headers(req),
				"Content-Type", "image/gif");
			evbuffer_add_reference(evb,
				(const void *)empty_gif, sizeof(empty_gif), NULL, NULL);
			evhttp_send_reply(req, HTTP_OK, "OK", evb);
			evbuffer_free(evb);
		}
	}
}

/* This callback gets invoked when we get any http request that doesn't match
 * any other callback.  Like any evhttp server callback, it has a simple job:
 * it must eventually call evhttp_send_error() or evhttp_send_reply().
 */
static void
send_document_cb(struct evhttp_request *req, void *arg)
{
	struct evbuffer *evb = NULL;
	const char *docroot = arg;
	const char *uri = evhttp_request_get_uri(req);
	struct evhttp_uri *decoded = NULL;
	const char *path;
	const char *type;
	char *decoded_path = NULL;
	char *whole_path = NULL;
	size_t len;
	int fd = -1;
	struct stat st;

	++request_count;

	if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
		return;
	}

	/* Decode the URI */
	decoded = evhttp_uri_parse(uri);
	if (!decoded) {
		evhttp_send_error(req, HTTP_BADREQUEST, 0);
		return;
	}

	/* Let's see what path the user asked for. */
	path = evhttp_uri_get_path(decoded);
	if (!path) path = "/";

	/* We need to decode it, to see what path the user really wanted. */
	decoded_path = evhttp_uridecode(path, 0, NULL);
	
	/* Don't allow any ".."s in the path, to avoid exposing stuff outside
	 * of the docroot.  This test is both overzealous and underzealous:
	 * it forbids aceptable paths like "/this/one..here", but it doesn't
	 * do anything to prevent symlink following." */
	if (strstr(decoded_path, "..")) goto fobidden;

	len = strlen(docroot) + strlen(decoded_path) + 2;
	if (!(whole_path = malloc(len))) goto internalerr;

	evutil_snprintf(whole_path, len, "%s/%s", docroot, decoded_path);
	if (stat(whole_path, &st) < 0) goto notfound;

	/* Silent is Gold */
	if (S_ISDIR(st.st_mode)) goto fobidden;

	/* It's a file; add it to the buffer to get
	 * sent via sendfile */
	type = guess_content_type(decoded_path);
	if (!type) goto fobidden;

	if ((fd = open(whole_path, O_RDONLY)) < 0) goto notfound;

	if (fstat(fd, &st) < 0) {
		/* Make sure the length still matches, now that we
		 * opened the file :/ */
		goto notfound;
	}

	/* This holds the content we're sending. */
	evb = evbuffer_new();
	if (!evb) goto internalerr;

	evhttp_add_header(evhttp_request_get_output_headers(req),
		"Content-Type", type);
	evbuffer_add_file(evb, fd, 0, st.st_size);
	evhttp_send_reply(req, HTTP_OK, "OK", evb);
	goto done;

internalerr:
	evhttp_send_error(req, HTTP_INTERNAL, 0);
	if (fd >= 0)
		close(fd);
	goto done;

notfound:
	evhttp_send_error(req, HTTP_NOTFOUND, "Document was not found");
	if (fd >= 0)
		close(fd);
	goto done;

fobidden:
	evhttp_send_error(req, HTTP_FOBIDDEN, "Fobidden");

done:
	if (decoded)
		evhttp_uri_free(decoded);
	if (decoded_path)
		free(decoded_path);
	if (whole_path)
		free(whole_path);
	if (evb)
		evbuffer_free(evb);
}

static void
signal_cb(evutil_socket_t fd, short event, void *arg)
{
	struct logbuf_t *buf = logbuf;

	while (buf->count > 0)
		usleep(50 * 1000); // 50ms

	logbuf = NULL;
	pthread_join(log_thr, NULL);

	close(log_fd);
	exit(1);
}

static void
syntax(void)
{
	fprintf(stdout, "Syntax: hoopstatserver <docroot> [bind-ip] [port]\n");
}

static void
null_eventlog_cb(int severity, const char *msg)
{
}

int
main(int argc, char **argv)
{
	struct event_base *base;
	struct event *signal_int;
	struct event *signal_term;
	struct evhttp *http;
	struct evhttp_bound_socket *handle;

	const char *bindif = "0.0.0.0";
	unsigned short port = HTTP_DEFAULT_PORT;

	if (argc < 2 || argc > 4) {
		syntax();
		return 1;
	}
	
	if (argc >= 3)
		bindif = argv[2];
	
	if (argc >= 4) {
		unsigned short pp =
			(unsigned short)strtoul(argv[3], (char **)NULL, 10);

		if (pp > 0)
			port = pp;
		else {
			syntax();
			return 1;
		}
	}

	/* Ignore SIGPIPE */
	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) return 1;

	base = event_base_new();
	if (!base) {
		fprintf(stderr, "Couldn't create an event_base: exiting\n");
		return 1;
	}

	/* Initalize single events */
	signal_int = event_new(base, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb,
	    NULL);
	if (!signal_int) {
		fprintf(stderr, "Couldn't create SIGINT event: exiting\n");
		return 1;
	}
	
	signal_term = event_new(base, SIGTERM, EV_SIGNAL|EV_PERSIST, signal_cb,
	    NULL);
	if (!signal_term) {
		fprintf(stderr, "Couldn't create SIGTERM event: exiting\n");
		return 1;
	}

	/* Add events to loop */
	event_add(signal_int, NULL);
	event_add(signal_term, NULL);

	/* Create a new evhttp object to handle requests. */
	http = evhttp_new(base);
	if (!http) {
		fprintf(stderr, "couldn't create evhttp. Exiting.\n");
		return 1;
	}

	/* The /log URI will log access. */
	evhttp_set_cb(http, "/log", log_request_cb, NULL);

	/* The /ping URI will only say 200 ok. */
	evhttp_set_cb(http, "/ping", ping_request_cb, NULL);

	/* The /empty_gif will send back an empty gif. */
	evhttp_set_cb(http, "/empty_gif", emptygif_request_cb, NULL);

	/* We want to accept arbitrary requests, so we need to set a "generic"
	 * cb.  We can also add callbacks for specific paths. */
	evhttp_set_gencb(http, send_document_cb, argv[1]);

	/* Now we tell the evhttp what port to listen on */
	handle = evhttp_bind_socket_with_handle(http, bindif, port);
	if (!handle) {
		fprintf(stderr, "couldn't bind on %s:%d. Exiting.\n",
		    bindif, (int)port);
		return 1;
	}

	/* Extract and display the address we're listening on. */
	struct sockaddr_storage ss;
	evutil_socket_t fd;
	ev_socklen_t socklen = sizeof(ss);
	char addrbuf[128];
	void *inaddr;
	const char *addr;
	int got_port = -1;

	fd = evhttp_bound_socket_get_fd(handle);
	memset(&ss, 0, sizeof(ss));

	if (getsockname(fd, (struct sockaddr *)&ss, &socklen)) {
		fprintf(stderr, "getsockname() failed. Exiting.\n");
		return 1;
	}

	if (ss.ss_family == AF_INET) {
		got_port = ntohs(((struct sockaddr_in*)&ss)->sin_port);
		inaddr = &((struct sockaddr_in*)&ss)->sin_addr;
	}
	else if (ss.ss_family == AF_INET6) {
		got_port = ntohs(((struct sockaddr_in6*)&ss)->sin6_port);
		inaddr = &((struct sockaddr_in6*)&ss)->sin6_addr;
	}
	else {
		fprintf(stderr, "Weird address family %d\n",
			ss.ss_family);
		return 1;
	}

	addr = evutil_inet_ntop(ss.ss_family, inaddr, addrbuf,
		sizeof(addrbuf));
	if (addr)
		fprintf(stdout, "Listening on %s:%d\n", addr, got_port);
	else {
		fprintf(stderr, "evutil_inet_ntop failed\n");
		return 1;
	}

	/* Ignore libevent logs */
	event_set_log_callback(null_eventlog_cb);

	char logfile[128];
	evutil_snprintf(logfile, sizeof(logfile), "%s:%d", addr, got_port);

	log_fd = open(logfile, O_WRONLY | O_APPEND | O_CREAT, 0666);
	if (log_fd < 0) {
		fprintf(stderr, "log file open failed\n");
		return 1;
	}

	logbuf0.evb = evbuffer_new();
	logbuf1.evb = evbuffer_new();
	if (!logbuf0.evb || !logbuf1.evb) {
		fprintf(stderr, "log buf create failed\n");
		return 1;
	}

	if (pthread_create(&log_thr, NULL, write_log_thread, NULL) != 0) {
		fprintf(stderr, "log thread create failed\n");
		return 1;
	}

	event_base_dispatch(base);
	
	return 0;
}
